package com.bingxue.edu.management.generator.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.system.SystemUtil;
import com.bingxue.edu.framework.common.util.CheckUtils;
import com.bingxue.edu.framework.common.util.FileUploadUtils;
import com.bingxue.edu.framework.exception.BusinessException;
import com.bingxue.edu.management.generator.config.GeneratorProperties;
import com.bingxue.edu.management.generator.enums.QueryTypeEnum;
import com.bingxue.edu.management.generator.model.entity.FieldConfig;
import com.bingxue.edu.management.generator.model.entity.GenConfig;
import com.bingxue.edu.management.generator.model.resp.GeneratePreviewResp;
import com.bingxue.edu.management.generator.service.GeneratorService;
import com.bingxue.edu.management.generator.service.TableMetaService;
import com.bingxue.edu.management.generator.util.TemplateUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * 代码生成业务实现
 *
 * @author panbo
 * @since 2024/7/7
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class GeneratorServiceImpl implements GeneratorService {

    private final GeneratorProperties generatorProperties;
    private final TableMetaService tableMetaService;

    @Override
    public List<GeneratePreviewResp> preview(String tableName) {
        List<GeneratePreviewResp> generatePreviewList = new ArrayList<>();
        // 表配置
        GenConfig genConfig = tableMetaService.getGenConfigById(tableName);
        CheckUtils.throwIfNull(genConfig, "请先进行数据表 [{}] 生成配置", tableName);
        // 字段配置
        List<FieldConfig> fieldConfigList = tableMetaService.listFieldConfigByTableName(tableName);
        CheckUtils.throwIfEmpty(fieldConfigList, "请先进行数据表 [{}] 字段配置", tableName);
        // 生成配置，在模板文件中使用
        Map<String, Object> genConfigMap = BeanUtil.beanToMap(genConfig);
        // 用于 javadocs 日期
        genConfigMap.put("date", DateUtil.date().toString("yyyy/MM/dd HH:mm"));
        // 包名，com.bingxue.edu.system
        String packageName = genConfig.getPackageName();
        // 模块名，取的是包名最后部分，system
        String moduleName = StrUtil.subSuf(packageName, StrUtil.lastIndexOfIgnoreCase(packageName, StrUtil.DOT) + 1);
        String entityClassName = genConfig.getEntityClassName();
        genConfigMap.put("moduleName", moduleName); // system
        genConfigMap.put("EntityName", entityClassName); // User
        genConfigMap.put("entityName", StrUtil.lowerFirst(entityClassName));
        // 判断实体类父类
        String baseEntity = "BaseEntity";
        if (fieldConfigList.stream().anyMatch(item -> item.getFieldName().equals("deleted"))) {
            baseEntity = "BusinessEntity";
        } else if (fieldConfigList.stream().anyMatch(item -> item.getFieldName().equals("createdBy"))) {
            baseEntity = "AuditableEntity";
        }
        genConfigMap.put("baseEntity", baseEntity);
        // 渲染代码
        Map<String, GeneratorProperties.TemplateConfig> templateConfigMap = generatorProperties.getTemplateConfigs();
        // 遍历每一个模板文件
        for (Map.Entry<String, GeneratorProperties.TemplateConfig> templateConfigEntry : templateConfigMap.entrySet()) {
            this.pretreatment(genConfigMap, fieldConfigList, templateConfigEntry);
            String className = entityClassName;
            // 去除Entity, 实体类保留本身entityClassName
            if(!"Entity".equals(templateConfigEntry.getKey())) {
                // Employee + Controller = EmployeeController
                className = entityClassName + StrUtil.nullToEmpty(templateConfigEntry.getKey());
            }
            // 每个模板文件中的className均不同，指的是文件类名
            genConfigMap.put("className", className);

            GeneratorProperties.TemplateConfig templateConfig = templateConfigEntry.getValue();

            boolean isBackend = templateConfig.isBackend();
            String extension = templateConfig.getExtension();

            GeneratePreviewResp generatePreview = new GeneratePreviewResp();
            generatePreview.setBackend(isBackend);
            generatePreviewList.add(generatePreview);
            if (isBackend) {
                generatePreview.setFileName(className + extension);
            } else {
                // index.vue 和 className无关
                boolean isIndexVue = ".vue".equals(extension) && "index".equals(templateConfigEntry.getKey());
                generatePreview.setFileName(isIndexVue ? "index.vue" : this.getFrontendFileName(entityClassName, className, extension));
                genConfigMap.put("fieldConfigs", fieldConfigList);
            }
            generatePreview.setContent(TemplateUtils.render(templateConfig.getTemplatePath(), genConfigMap));
            setPreviewPath(generatePreview, genConfig, templateConfig);
        }
        return generatePreviewList;
    }

    private void setPreviewPath(GeneratePreviewResp generatePreview,
                                GenConfig genConfig,
                                GeneratorProperties.TemplateConfig templateConfig) {
        // 获取前后端基础路径 app-backend/src/main/java/com/xxx/yyy
        String backendBaseCodePath = this.buildBackendPreviewBasePath(genConfig);
        // 获取前后端基础路径 app-web/src
        String frontendBaseCodePath = this.buildFrontendPreviewBasePath(genConfig);
        String packageName = genConfig.getPackageName();
        String moduleName = StrUtil.subSuf(packageName, StrUtil.lastIndexOfIgnoreCase(packageName, StrUtil.DOT) + 1);
        String packagePath;
        if (generatePreview.isBackend()) {
            packagePath = String.join(File.separator, backendBaseCodePath, templateConfig.getPackageName().replace(StrUtil.DOT, File.separator));
        } else {
            packagePath = String.join(File.separator, frontendBaseCodePath, templateConfig.getPackageName().replace(StrUtil.SLASH, File.separator), moduleName);
            packagePath = ".vue".equals(templateConfig.getExtension())
                ? packagePath + File.separator + StrUtil.lowerFirst(genConfig.getEntityClassName())
                : packagePath;
        }
        generatePreview.setPath(packagePath);
    }

    @Override
    public void download(List<String> tableNames, HttpServletRequest request, HttpServletResponse response) {
        try {
            String tempDir = SystemUtil.getUserInfo().getTempDir();
            String appName = getAbsoluteProjectBasePath().getFileName().toString();
            // 删除旧代码
            FileUtil.del(tempDir + appName);
            tableNames.forEach(tableName -> {
                // 初始化配置及数据
                List<GeneratePreviewResp> generatePreviewList = this.preview(tableName);
                // 生成代码
                Path generatePath = Paths.get(SystemUtil.getUserInfo().getTempDir(), appName);
                this.generateCode(generatePath, generatePreviewList, tableMetaService.getGenConfigById(tableName));
            });
            // 打包下载
            File tempDirFile = new File(tempDir, appName);
            String zipFilePath = tempDirFile.getPath() + jodd.io.ZipUtil.ZIP_EXT;
            ZipUtil.zip(tempDirFile.getPath(), zipFilePath);
            FileUploadUtils.download(request, response, new File(zipFilePath), true);
        } catch (Exception e) {
            log.error("Generate code of table '{}' occurred an error. {}", tableNames, e.getMessage(), e);
            throw new BusinessException("代码生成失败，请手动清理生成文件");
        }
    }

    @Override
    public void generate(List<String> tableNames) {
        try {
            tableNames.forEach(tableName -> {
                // 初始化配置及数据
                List<GeneratePreviewResp> generatePreviewList = this.preview(tableName);
                // 生成代码
                Path projectPath = getAbsoluteProjectBasePath();
                this.generateCode(projectPath, generatePreviewList, tableMetaService.getGenConfigById(tableName));
                log.info("代码生成成功");
            });
        } catch (Exception e) {
            log.error("Generate code of table '{}' occurred an error. {}", tableNames, e.getMessage(), e);
            throw new BusinessException("代码生成失败，请手动清理生成文件");
        }
    }

    /**
     * 生成代码
     *
     * @param basePath 生成代码基础路径
     * @param generatePreviewList 生成预览列表
     * @param genConfig           生成配置
     */
    private void generateCode(Path basePath, List<GeneratePreviewResp> generatePreviewList, GenConfig genConfig) {
        for (GeneratePreviewResp generatePreview : generatePreviewList) {
            File file = basePath.resolve(generatePreview.getPath()).resolve(generatePreview.getFileName()).toFile();
            // 如果已经存在，且不允许覆盖，则跳过
            if (!file.exists() || Boolean.TRUE.equals(genConfig.getIsOverride())) {
                log.info("生成代码: {}", file.getAbsoluteFile());
                FileUtil.writeUtf8String(generatePreview.getContent(), file);
            }
        }
    }

    /**
     * 构建后端包预览路径
     * /Users/xxx/app/app-backend/src/main/java/com/xxx/
     * @param genConfig 生成配置
     * @return 后端包路径
     */
    private String buildBackendPreviewBasePath(GenConfig genConfig) {
        // /Users/xxx/app/app-backend
        String basePath = generatorProperties.getBackendBasePath();
        // 只保留app-backend
        basePath = StrUtil.subSuf(basePath, StrUtil.lastIndexOfIgnoreCase(basePath, StrUtil.SLASH) + 1);
        // 添加src/main/java/com/xxx/yyy
        return Paths.get(basePath, "src/main/java", genConfig.getPackageName().replace(StrUtil.DOT, File.separator)).toString();
    }

    /**
     * 构建前端包预览路径
     *
     * @param genConfig 生成配置
     * @return 前端包路径
     */
    private String buildFrontendPreviewBasePath(GenConfig genConfig) {
        // /Users/xxx/app/app-web
        String basePath = generatorProperties.getFrontendBasePath();
        // 只保留app-web
        basePath = StrUtil.subSuf(basePath, StrUtil.lastIndexOfIgnoreCase(basePath, StrUtil.SLASH) + 1);
        // 添加src
        return Paths.get(basePath, "src").toString();
    }

    /**
     * 获取项目绝对路径
     *
     * @return 项目绝对路径
     */
    private Path getAbsoluteProjectBasePath() {
        // /Users/xxx/app/app-web
        String basePath = generatorProperties.getFrontendBasePath();
        return Paths.get(basePath).getParent();
    }

    /**
     * 获取前端文件名
     *
     * @param classNamePrefix 类名前缀
     * @param className       类名
     * @param extension       扩展名
     * @return 前端文件名
     */
    private String getFrontendFileName(String classNamePrefix, String className, String extension) {
        // ts文件camelCase风格，其余（vue组件）大写风格
        return (".ts".equals(extension) ? StrUtil.lowerFirst(classNamePrefix) : className) + extension;
    }

    /**
     * 预处理生成配置
     *
     * @param genConfigMap          生成配置
     * @param originFieldConfigList 原始字段配置列表
     * @param templateConfigEntry   模板配置
     */
    private void pretreatment(Map<String, Object> genConfigMap,
                              List<FieldConfig> originFieldConfigList,
                              Map.Entry<String, GeneratorProperties.TemplateConfig> templateConfigEntry) {
        GeneratorProperties.TemplateConfig templateConfig = templateConfigEntry.getValue();
        // 移除需要忽略的字段，每个模板文件的忽略字段列表不一样，通过properties配置的
        List<FieldConfig> fieldConfigList = originFieldConfigList.stream()
            .filter(fieldConfig -> !StrUtil.equalsAny(fieldConfig.getFieldName(), templateConfig.getExcludeFields()))
            .toList();
        // 自定义类列表，枚举类
        List<String> customClassList = new LinkedList<>();
        genConfigMap.put("fieldConfigs", fieldConfigList);
        // 统计部分特殊字段特征
        genConfigMap.put("hasLocalDateTime", false);
        genConfigMap.put("hasBigDecimal", false);
        genConfigMap.put("hasRequiredField", false);
        genConfigMap.put("hasListQueryField", false);
        for (FieldConfig fieldConfig : fieldConfigList) {
            String fieldType = fieldConfig.getFieldType();
            if(fieldConfig.getComment().startsWith("enum-")) {
                fieldConfig.setComment(fieldConfig.getComment().replace("enum-", ""));
            }
            if ("LocalDateTime".equals(fieldType)) {
                genConfigMap.put("hasLocalDateTime", true);
            }
            if ("BigDecimal".equals(fieldType)) {
                genConfigMap.put("hasBigDecimal", true);
            }
            if (fieldType.endsWith("Enum")) {
                customClassList.add("com.bingxue.edu.common.enums." + fieldType);
            }
            if (fieldConfig.getIsRequired()) {
                genConfigMap.put("hasRequiredField", true);
            }
            QueryTypeEnum queryType = fieldConfig.getQueryType();
            if (null != queryType && StrUtil.equalsAny(queryType.name(), QueryTypeEnum.IN.name(), QueryTypeEnum.NOT_IN
                .name(), QueryTypeEnum.BETWEEN.name())) {
                genConfigMap.put("hasListQueryField", true);
            }
        }
        String subPackageName = templateConfig.getPackageName();
        genConfigMap.put("subPackageName", subPackageName);
        genConfigMap.put("customClassList", customClassList);
    }
}
